Custom Presets use a Lua script to define an effect that can be played back on a Matrix. You can use this to create effects that are not available as standard in Designer. Custom Presets are managed using the Media window.
Custom presets use Lua scripts to define an animation.
For each pixel (x,y) of each frame of that animation, a pixel
function is called which returns three numbers, between 0 and 255, which represent the red, green and blue components of the colour of that pixel. Pixel (0,0) is in the top left of the frame, with the positive x axis pointing right and the positive y axis pointing down.
Here is the most simple example of a custom preset:
function pixel(frame,x,y) return 255,0,0 end
This fills every pixel of every frame with red. If you do not return all three components of the pixel's colour, the missing components are assumed to be 0, so the following function is equivalent to Listing 1:
function pixel(frame,x,y) return 255 end
To demonstrate what can be achieved with custom presets, we are going to build up a real example as concepts are introduced throughout this guide.
To start, we are going to create a preset that renders a series of vertical red bands:
-- width of the bands in pixels band_width = 4 -- space between bands in pixels band_spacing = 1 -- modulo operator (a%b) function mod(a,b) return a - math.floor(a/b)*b end-- the pixel function function pixel(frame,x,y) -- use the modulo operator to split the horizontal axis into bands and -- decide if we are in the band or in the separator between bands if (mod(x,band_width+band_spacing)<band_width) then -- in band return 255,0,0 else -- in band separator return 0,0,0 end end
You will note that we have defined a new function, mod
, to implement the modulo operator. This was done to make the script more readable. We will discuss user-defined functions again later.
We also defined two variables, band_width
and band_spacing
. These we placed outside of the pixel
function because they are the same for every pixel of every frame of the effect, so it is more efficient to not execute the assignment for every pixel. Any code outside of the pixel function is executed once, before the pixel
function is called for the first time.
Filling every frame of an animation with a single colour is not very exciting, so we can use the frame
argument to change the colour of a given pixel (x,y) based on the current frame.
Here is an example:
function pixel(frame,x,y) if (x<frame) then return 255,0,0 else return 0,0,0 end end
This creates a red horizontal wipe, advancing 1 pixel towards the right for each frame. You may have noted that once the wipe reaches the right side of the frame, the whole frame stays red for a period of time before the animation loops back to the beginning. This is because the number of frames exceeded the number of pixels across the frame.
Ideally, we want our effects to loop seamlessly. To do this, we introduce three global variables that have been already been defined for you:
frames
- the total number of frames in the animationwidth
- the width of the animation in pixelsheight
- the height of the animation in pixelsWe can rewrite Listing 4 as follows:
function pixel(frame,x,y) -- calculate the progress through the animation local t = frame/frames -- compare the fraction across the effect with the animation progress if (x/width<t) then return 255,0,0 else return 0,0,0 end end
Now, once the red wipe reaches the right side of the frame, it immediately jumps back to the start. Returning to our vertical band example, we are going to introduce animation by changing the height of each band over time:
-- width of the bands in pixels band_width = 4 -- space between bands in pixels band_spacing = 1-- get the combined width of band and separator local total_band_width = band_width+band_spacing -- get the number of visible bands local bands = width/total_band_width-- modulo operator (a%b) function mod(a,b) return a - math.floor(a/b)*b end-- the pixel function function pixel(frame,x,y)if (mod(x,total_band_width)>=band_width) then -- in band separator return 0,0,0 end-- get the band in which this pixel falls local band = math.floor(x/total_band_width)-- get the fraction through the effect local t = frame/frames-- get the height of the band in which this pixel falls local band_height = (math.sin((band/bands+t)*math.pi*2)+1)/2-- adjust y to be relative to the center of the effect y = y-(height/2)+0.5-- decide if this pixel is inside the band if (math.abs(y)/(height/2) <= band_height) then return 255,0,0 else return 0,0,0 end end
We are using a sine function to set the height of each band, where the argument to the sine function is offset based on the index of the band and the current fraction through the effect. The result of this is that the height of each band differs from its neighbour according the sine function, and this relationship is modified over time to create a ripple.
So far, we have just been creating red effects, but there are more colours than red, so why should we stick with that?
We will modify the vertical band example to show how different colours can be created. For this example, we introduce the built-in function, hsi_to_rgb
, which converts an HSI (hue, saturation, intensity) colour into an RGB (red, green, blue) colour:
-- width of the bands in pixels band_width = 4 -- space between bands in pixels band_spacing = 1-- get the combined width of band and separator local total_band_width = band_width+band_spacing -- get the number of visible bands local bands = width/total_band_width-- modulo operator (a%b) function mod(a,b) return a - math.floor(a/b)*b end-- rainbow lookup function rainbow(hue) return hsi_to_rgb(hue*math.pi*2,1,1) end-- the pixel function function pixel(frame,x,y)if (mod(x,total_band_width)>=band_width) then -- in band separator return 0,0,0 end-- get the band in which this pixel falls local band = math.floor(x/total_band_width)-- get the fraction through the effect local t = frame/frames-- get the height of the band in which this pixel falls local band_height = (math.sin((band/bands+t)*math.pi*2)+1)/2-- adjust y to be relative to the center of the effect y = y-(height/2)+0.5-- decide if this pixel is inside the band local h = math.abs(y)/(height/2) if (h <= band_height) then return rainbow(band/bands+t) else -- offset hue by quarter return rainbow((band/bands+t)+0.25) end end
We have defined a new function, rainbow
, which returns a fully saturated r,g,b value for a given hue. This function is then called with different arguments depending on whether on not a pixel falls inside or outside of a band.
User-defined functions can be used whenever you want to use a similar piece of code in multiple places with differing arguments.
Running this script, you will see that the bands are now coloured with a rainbow which changes over time, and the area above and below the band is filled with a colour that is pi/2 radians out of phase with the band's colour.
Working with colours as 3 separate components can produce a wide variety of effects, but sometimes it is more convenient to treat a colour as a single entity. We can do that with the colour library.
To create a variable of type colour, call colour.new()
, passing in three values between 0 and 255 which represent the red, green and blue components of the colour, i.e:
local c = colour.new(255,0,0)
The variable c
has the type colour and represents red. Colours have three properties, red
, green
and blue
, which can be used to access and alter that colour. Here is a simple example using the colour type:
function pixel(frame,x,y) local c = colour.new(255,0,0) return c.red,c.green,c.blue end
This fills every pixel of every frame with red.
Earlier in this document, we stated that the pixel
function should return 3 numbers, representing the red, green and blue components of a colour. This was not the entire truth. We are also allowed to return a single variable of type colour.
This function is therefore equivalent to Listing 8:
function pixel(frame,x,y) local c = colour.new(255,0,0) return c end
Once again, we return to our vertical band example and use colour variables to specify the band colour and the background colour:
-- width of the bands in pixels band_width = 4 -- space between bands in pixels band_spacing = 1 -- the colour of the band band_colour = colour.new(255,0,0) -- the colour of the space between bands background_colour = colour.new(0,0,255)-- get the combined width of band and separator local total_band_width = band_width+band_spacing -- get the number of visible bands local bands = width/total_band_width-- modulo operator (a%b) function mod(a,b) return a - math.floor(a/b)*b end-- the pixel function function pixel(frame,x,y)if (mod(x,total_band_width)>=band_width) then -- in band separator return background_colour end-- get the band in which this pixel falls local band = math.floor(x/total_band_width)-- get the fraction through the effect local t = frame/frames-- get the height of the band in which this pixel falls local band_height = (math.sin((band/bands+t)*math.pi*2)+1)/4-- adjust y to be relative to the center of the effect y = y-(height/2)+0.5-- decide if this pixel is inside the band if (math.abs(y)/height<=band_height) then return band_colour else return background_colour end end
We have added two variables, band_colour
(red) and background_colour
(blue) and are now returning those values rather than the r,g,b values that we were using previously. You should now see red bands rippling over a blue background.
The colour library also includes an interpolate
function, which takes two colours and a fraction and returns a new colour that is linearly interpolated between the two colours. For example:
local red = colour.new(255,0,0) local blue = colour.new(0,0,255)function pixel(frame,x,y) -- interpolate between red and blue using the horizontal displacement of x -- note that we use (width-1) so the rightmost pixel is completely blue return colour.interpolate(red,blue,x/(width-1)) end
This creates a horizontal red to blue gradient. We could have created the same gradient without the colour library as follows:
function pixel(frame,x,y) local f = x/(width-1) return 255*(1-f),0,(255*f) end
However, if you changed your mind about the colours that you wanted for your gradient, it would be significantly harder to alter Listing 12 than it would be to change the colours in the first two lines of Listing 11.
The gradient library adds support for more complicated gradients that cannot be achieved by interpolating between two colours.
To create a new variable of type gradient, call gradient.new()
, passing in two colours, i.e:
local c1 = colour.new(255,0,0) local c2 = colour.new(0,0,255) local g = gradient.new(c1, c2)
To find the colour of the gradient at a specific point, use the lookup
function, passing in a number between 0 and 1. For example:
local red = colour.new(255,0,0) local blue = colour.new(0,0,255) local g = gradient.new(red, blue)function pixel(frame,x,y) -- note the use of the colon operator return g:lookup(x/(width-1)) end
This creates a horizontal gradient from red to blue, but we have already seen that there are other ways to generate the same result which will probably be more efficient. To show where the gradient library offers more power:
local red = colour.new(255,0,0) local blue = colour.new(0,0,255) local g = gradient.new(red,blue)-- add a third point to the middle of the gradient local green = colour.new(0,255,0) g:add_point(0.5,green)function pixel(frame,x,y) return g:lookup(x/(width-1)) end
We used the add_point
function to insert a green colour midway between the red and the blue colours. This generates a horizontal gradient that fades from red to green to blue.
Back to the vertical band example, we will use a gradient to colour the bands:
Listing 15
-- width of the bands in pixels band_width = 4 -- space between bands in pixels band_spacing = 1 -- the colour of the band band_gradient = gradient.new(colour.new(255,0,0), colour.new(255,255,0)) -- the colour of the space between bands background_colour = colour.new(0,0,0)-- get the combined width of band and separator local total_band_width = band_width+band_spacing -- get the number of visible bands local bands = width/total_band_width-- modulo operator (a%b) function mod(a,b) return a - math.floor(a/b)*b end-- the pixel function function pixel(frame,x,y)if (mod(x,total_band_width)>=band_width) then -- in band separator return background_colour end-- get the band in which this pixel falls local band = math.floor(x/total_band_width)-- get the fraction through the effect local t = frame/frames-- get the height of the band in which this pixel falls local band_height = (math.sin((band/bands+t)*math.pi*2)+1)/2-- adjust y to be relative to the center of the effect y = y-(height/2)+0.5-- decide if this pixel is inside the band local h = math.abs(y)/(height/2) if (h<=band_height) then return band_gradient:lookup(h) else return background_colour end end
The band_gradient
variable is initialised as a red to yellow gradient, and we use band_gradient:lookup(h)
to determine the colour of the band at height h.
Custom presets can have properties which will be exposed in Designer whenever the preset is placed on a timeline. This allows a single custom preset to create a wide variety of effects. It also means that you do not have to create near-identical copies of custom presets just to change one parameter, for example, a colour. You can just expose a colour property and specify the desired colour when the preset is placed on a timeline.
To define a property, you would call the function:
property(name, type, default_value, ...)
This must be added to your script outside of any function call.
name
is a string and must be unique within a custom preset and must not contain spaces. This name will be used as the name of a global variable that is available in your script, whose value will depend on what has been set for a given instance of your custom preset.
type
is the type of the property. It can be one of the following values: BOOLEAN
, INTEGER
, FLOAT
, COLOUR
and GRADIENT
. This determines what sort of control is presented to the user when placing a custom preset on a timeline.
default_value
is the initial value of a property when first added to a timeline. The value passed in here depends on the type of the property, and this is outlined below.
Certain types of properties also allow some addition arguments to be specified, and these will also be described for each type below:
property("invert", BOOLEAN, true)
The default value should be true
or false
.
property("count", INTEGER, number, [min], [max], [step])
The default value should be a number between min
and max
.
min
is the minimum allowed value (default: -2147483648)max
is the maximum allowed value (default: 2147483647)step
is the difference between allowed values (default: 1)min
, max
and step
are optional.
property("count", FLOAT, number, [min], [max], [resolution])
The default value should be a number between min
and max
.
min
is the minimum allowed valuemax
is the maximum allowed valueresolution
is the number of decimal places to display (default: 2)min
, max
and resolution
are optional.
property("background", COLOUR, red, green, blue)
red
, green
and blue
are the default values of the components of the colour.
property("gradient", GRADIENT, {fraction, red, green, blue}, ...)
The default value of a gradient is a list of fractions and colours, where fraction
is in the range [0-1] and specifies where in the gradient the colour is, and red
, green
and blue
is the colour at that position and are in the range [0-255]. You can specify multiple points. For example:
property("gradient", GRADIENT, 0.0, 255, 0, 0, 1.0, 0, 0, 255)
creates a red (255,0,0) point at the start (0.0) and a blue ((0,0,255) point at the end (1.0).
To demonstrate a real example of using properties in scripts:
property("g", GRADIENT, 0.0, 255, 0, 0, 1.0, 0, 0, 255)function pixel(frame,x,y) return g:lookup(x/(width-1)) end
This, by default, creates a horizontal gradient from red to blue, as we saw in Listing 13. However, when this preset is placed on a timeline, there will be a gradient editor available, and you will be able to alter the gradient to be any colour you wish, without having to recompile the script or having to duplicate the custom preset with some small alterations.
We will now modify our vertical band example to expose some properties to make a very versatile effect:
-- width of the bands in pixels property("band_width", INTEGER, 4, 1) -- space between bands in pixels property("band_spacing", INTEGER, 1, 0) -- the wavelength of the ripple (in terms of current width) property("wavelength", FLOAT, 1, 0, 16, 2) -- the direction of the ripple property("reverse", BOOLEAN, false) -- the colour of the band property("band_gradient", GRADIENT, 0, 255, 0, 0, 1, 255, 255, 0) -- the colour of the space between bands property("background_colour", COLOUR, 0, 0, 0)-- get the combined width of band and separator local total_band_width = band_width+band_spacing -- get the number of visible bands local bands = width/total_band_width-- modulo operator (a%b) function mod(a,b) return a - math.floor(a/b)*b end-- the pixel function function pixel(frame,x,y)if (mod(x,total_band_width)>=band_width) then -- in band separator return background_colour end-- get the band in which this pixel falls local band = math.floor(x/total_band_width)-- get the fraction through the effect local t = frame/frames-- optionally reverse the ripple if (reverse) then t = -t end-- get the height of the band in which this pixel falls local band_height = (math.sin((band/bands/wavelength+t)*math.pi*2)+1)/2-- adjust y to be relative to the center of the effect y = y-(height/2)+0.5-- decide if this pixel is inside the band local h = math.abs(y)/(height/2) if (h<=band_height) then return band_gradient:lookup(h) else return background_colour end end
You will notice that adding properties to the example involved little more than changing the variable definitions at the start of the script. There are also two new properties, wavelength
, for setting the wavelength of the ripple, and reverse
, for changing the direction of the ripple.
By adjusting the values of the properties, we can now create a variety of different effects without having to alter the script again.
colour.new(r,g,b)
Returns a new colour that represents the RGB color specified by the components r
, g
and b
. r
, g
and b
will be limited to the range [0,255].
colour.interpolate(c1,c2,f)
Returns the colour that is linearly interpolated between colour c1
and colour c2
at fraction f
. f
can fall outside of the range [0,1] and the returned colour will be extrapolated accordingly.
c:red
The value of the red component [0-255] of colour c
.
c:green
The value of the green component [0-255] of colour c
.
c:blue
The value of the blue component [0-255] of colour c
.
gradient.new(c1,c2)
Returns a new gradient with colour c1
at the start and colour c2
at the end.
g:lookup(f)
Returns the colour at fraction f
through the gradient g
. f
will be limited to the range [0,1].
g:add_point(f, c)
Adds the colour c
to the gradient g
at fraction f
.
dist(x1,y1,x2,y2)
Returns the distance between coordinate (x1
,y1
) and coordinate (x2
,y2
)
dist_from_center(x,y)
Returns the distance between coordinate (x
,y
) and the center of the frame.
This is not the same as calling dist(x,y,width/2,height/2)
. It takes into account the fact that the center of the frame may fall in the middle of a pixel. For example, if width
and height
were equal to 5, the center of the frame is the center of the pixel at coordinate (2,2), but calling dist(2,2,width/2,height/2)
will return 0.707, which is the distance between the top left of pixel (2,2) and its center. Calling dist_from_center(2,2)
, where width
and height
are equal to 5, will return 0.
print(message)
Prints message
in the debugger's Output window.
You are advised to remove calls to this function when you have finished debugging because it will allow the script to run faster when used in programming.
rgb_to_hsi(red,green,blue)
Converts an RGB (red, green, blue) colour to an HSI (hue, saturation, intensity) colour. red
, green
and blue
are in the range [0-255]. Returns three numbers, hue is in [0-2PI] radian, saturation and intensity are in the range [0-1].
hsi_to_rgb(hue,saturation,intensity)
Converts an HSI (hue, saturation, intensity) colour into an RGB (red, green, blue) colour. hue
is in [0-2PI] radians, saturation
and intensity
are in the range [0-1]. Returns three numbers in the range [0-255].